/*
Copyright 2018-2024 New Vector Ltd.
Copyright 2015 OpenMarket Ltd

SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
 */

#import "MXKCallViewController.h"

@import MatrixSDK;

#import "MXKAppSettings.h"
#import "MXKSoundPlayer.h"
#import "MXKTools.h"
#import "NSBundle+MatrixKit.h"

#import "MXKSwiftHeader.h"

NSString *const kMXKCallViewControllerWillAppearNotification = @"kMXKCallViewControllerWillAppearNotification";
NSString *const kMXKCallViewControllerAppearedNotification = @"kMXKCallViewControllerAppearedNotification";
NSString *const kMXKCallViewControllerWillDisappearNotification = @"kMXKCallViewControllerWillDisappearNotification";
NSString *const kMXKCallViewControllerDisappearedNotification = @"kMXKCallViewControllerDisappearedNotification";
NSString *const kMXKCallViewControllerBackToAppNotification = @"kMXKCallViewControllerBackToAppNotification";

static const CGFloat kLocalPreviewMargin = 20;

@interface MXKCallViewController ()
{
    NSTimer *hideOverlayTimer;
    NSTimer *updateStatusTimer;
    
    Boolean isMovingLocalPreview;
    Boolean isSelectingLocalPreview;
    
    CGPoint startNewLocalMove;

    /**
     The popup showed in case of call stack error.
     */
    UIAlertController *errorAlert;
    
    // the room events listener
    id roomListener;
    
    // Observe kMXRoomDidFlushDataNotification to take into account the updated room members when the room history is flushed.
    id roomDidFlushDataNotificationObserver;
    
    // Observe AVAudioSessionRouteChangeNotification
    id audioSessionRouteChangeNotificationObserver;
    
    // Current alert (if any).
    UIAlertController *currentAlert;
    
    //  Current peer display name
    NSString *peerDisplayName;
}

@property (nonatomic, assign) Boolean isRinging;

@property (nonatomic, nullable) UIView *incomingCallView;

@property (nonatomic, strong) UITapGestureRecognizer *onHoldCallContainerTapRecognizer;

@end

@implementation MXKCallViewController
@synthesize backgroundImageView;
@synthesize localPreviewContainerView, localPreviewVideoView, localPreviewActivityView, remotePreviewContainerView;
@synthesize overlayContainerView, callContainerView, callerImageView, callerNameLabel, callStatusLabel;
@synthesize callToolBar, rejectCallButton, answerCallButton, endCallButton;
@synthesize callControlContainerView, speakerButton, audioMuteButton, videoMuteButton;
@synthesize backToAppButton, cameraSwitchButton;
@synthesize backToAppStatusWindow;
@synthesize mxCall;
@synthesize mxCallOnHold;
@synthesize onHoldCallerImageView;
@synthesize onHoldCallContainerView;

#pragma mark - Class methods

+ (UINib *)nib
{
    return [UINib nibWithNibName:NSStringFromClass(self.class)
                          bundle:[NSBundle bundleForClass:self.class]];
}

+ (instancetype)callViewController:(MXCall*)call
{
    MXKCallViewController *instance = [[[self class] alloc] initWithNibName:NSStringFromClass(self.class)
                                                                     bundle:[NSBundle bundleForClass:self.class]];
    
    // Load the view controller's view now (buttons and views will then be available).
    if ([instance respondsToSelector:@selector(loadViewIfNeeded)])
    {
        // iOS 9 and later
        [instance loadViewIfNeeded];
    }
    else if (instance.view)
    {
        // Patch: on iOS < 9.0, we load the view by calling its getter.
    }
    
    instance.mxCall = call;
    
    return instance;
}

#pragma mark -

- (void)finalizeInit
{
    [super finalizeInit];
    
    _playRingtone = YES;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    updateStatusTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateTimeStatusLabel) userInfo:nil repeats:YES];
    
    self.callerImageView.defaultBackgroundColor = [UIColor clearColor];
    self.backToAppButton.backgroundColor = [UIColor clearColor];
    self.audioMuteButton.backgroundColor = [UIColor clearColor];
    self.videoMuteButton.backgroundColor = [UIColor clearColor];
    self.resumeButton.backgroundColor = [UIColor clearColor];
    self.moreButton.backgroundColor = [UIColor clearColor];
    self.speakerButton.backgroundColor = [UIColor clearColor];
    self.transferButton.backgroundColor = [UIColor clearColor];
    
    [self.backToAppButton setImage:[NSBundle mxk_imageFromMXKAssetsBundleWithName:@"icon_backtoapp"] forState:UIControlStateNormal];
    [self.backToAppButton setImage:[NSBundle mxk_imageFromMXKAssetsBundleWithName:@"icon_backtoapp"] forState:UIControlStateHighlighted];
    [self.audioMuteButton setImage:[NSBundle mxk_imageFromMXKAssetsBundleWithName:@"icon_audio_unmute"] forState:UIControlStateNormal];
    [self.audioMuteButton setImage:[NSBundle mxk_imageFromMXKAssetsBundleWithName:@"icon_audio_mute"] forState:UIControlStateSelected];
    [self.videoMuteButton setImage:[NSBundle mxk_imageFromMXKAssetsBundleWithName:@"icon_video_unmute"] forState:UIControlStateNormal];
    [self.videoMuteButton setImage:[NSBundle mxk_imageFromMXKAssetsBundleWithName:@"icon_video_mute"] forState:UIControlStateSelected];
    [self.moreButton setImage:[NSBundle mxk_imageFromMXKAssetsBundleWithName:@"icon_call_more"] forState:UIControlStateNormal];
    [self.moreButton setImage:[NSBundle mxk_imageFromMXKAssetsBundleWithName:@"icon_call_more"] forState:UIControlStateSelected];
    [self.speakerButton setImage:[NSBundle mxk_imageFromMXKAssetsBundleWithName:@"icon_speaker_off"] forState:UIControlStateNormal];
    [self.speakerButton setImage:[NSBundle mxk_imageFromMXKAssetsBundleWithName:@"icon_speaker_on"] forState:UIControlStateSelected];
    
    // Localize string
    [answerCallButton setTitle:[VectorL10n answerCall] forState:UIControlStateNormal];
    [answerCallButton setTitle:[VectorL10n answerCall] forState:UIControlStateHighlighted];
    [rejectCallButton setTitle:[VectorL10n rejectCall] forState:UIControlStateNormal];
    [rejectCallButton setTitle:[VectorL10n rejectCall] forState:UIControlStateHighlighted];
    [endCallButton setTitle:[VectorL10n endCall] forState:UIControlStateNormal];
    [endCallButton setTitle:[VectorL10n endCall] forState:UIControlStateHighlighted];
    [_resumeButton setTitle:[VectorL10n resumeCall] forState:UIControlStateNormal];
    [_resumeButton setTitle:[VectorL10n resumeCall] forState:UIControlStateHighlighted];
    
    // Refresh call information
    self.mxCall = mxCall;
    
    // Listen to AVAudioSession activation notification if CallKit is available and enabled
    BOOL isCallKitAvailable = [MXCallKitAdapter callKitAvailable] && [MXKAppSettings standardAppSettings].isCallKitEnabled;
    if (isCallKitAvailable)
    {
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(handleAudioSessionActivationNotification)
                                                     name:kMXCallKitAdapterAudioSessionDidActive
                                                   object:nil];
    }
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:kMXCallKitAdapterAudioSessionDidActive object:nil];

    [self removeObservers];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    [[NSNotificationCenter defaultCenter] postNotificationName:kMXKCallViewControllerWillAppearNotification object:nil];
    
    [self updateLocalPreviewLayout];
    [self showOverlayContainer:YES];
    
    if (mxCall)
    {
        // Refresh call display according to the call room state.
        [self callRoomStateDidChange:^{
            // Refresh call status
            [self call:self->mxCall stateDidChange:self->mxCall.state reason:nil];
        }];

    }
    
    if (_delegate)
    {
        backToAppButton.hidden = NO;
    }
    else
    {
        backToAppButton.hidden = YES;
    }
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [[NSNotificationCenter defaultCenter] postNotificationName:kMXKCallViewControllerAppearedNotification object:nil];
    
    // trick to hide the volume at launch
    // as the mininum volume is forced by the application
    // the volume popup can be displayed
    //    volumeView = [[MPVolumeView alloc] initWithFrame: CGRectMake(5000, 5000, 0, 0)];
    //    [self.view addSubview: volumeView];
    //
    //    dispatch_after(dispatch_walltime(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
    //        [volumeView removeFromSuperview];
    //    });
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    [[NSNotificationCenter defaultCenter] postNotificationName:kMXKCallViewControllerWillDisappearNotification object:nil];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    [[NSNotificationCenter defaultCenter] postNotificationName:kMXKCallViewControllerDisappearedNotification object:nil];
}

- (void)dismiss
{
    if (_delegate)
    {
        [_delegate dismissCallViewController:self completion:nil];
    }
    else
    {
        // Auto dismiss after few seconds
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self dismissViewControllerAnimated:YES completion:nil];
        });
    }
}

#pragma mark - override MXKViewController

- (void)destroy
{
    self.peer = nil;
    
    self.mxCall = nil;
    
    _delegate = nil;
    
    self.isRinging = NO;
    
    [hideOverlayTimer invalidate];
    [updateStatusTimer invalidate];
    
    _incomingCallView = nil;
    
    _onHoldCallContainerTapRecognizer = nil;
    
    [super destroy];
}

#pragma mark - Properties

- (UIImage *)picturePlaceholder
{
    return [NSBundle mxk_imageFromMXKAssetsBundleWithName:@"default-profile"];
}

- (void)setMxCall:(MXCall *)call
{
    // Remove previous call (if any)
    if (mxCall)
    {
        mxCall.delegate = nil;
        mxCall.selfVideoView = nil;
        mxCall.remoteVideoView = nil;
        [self removeMatrixSession:self.mainSession];
        
        [self removeObservers];
        
        mxCall = nil;
    }
    
    if (call && call.room)
    {
        mxCall = call;
        
        [self addMatrixSession:mxCall.room.mxSession];

        MXWeakify(self);

        // Register a listener to handle messages related to room name, members...
        roomListener = [mxCall.room listenToEventsOfTypes:@[kMXEventTypeStringRoomName, kMXEventTypeStringRoomTopic, kMXEventTypeStringRoomAliases, kMXEventTypeStringRoomAvatar, kMXEventTypeStringRoomCanonicalAlias, kMXEventTypeStringRoomMember] onEvent:^(MXEvent *event, MXTimelineDirection direction, MXRoomState *roomState) {
            MXStrongifyAndReturnIfNil(self);

            // Consider only live events
            if (self->mxCall && direction == MXTimelineDirectionForwards)
            {
                // The room state has been changed
                [self callRoomStateDidChange:nil];
            }
        }];
        
        // Observe room history flush (sync with limited timeline, or state event redaction)
        roomDidFlushDataNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXRoomDidFlushDataNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
            MXStrongifyAndReturnIfNil(self);
            
            MXRoom *room = notif.object;
            if (self->mxCall && self.mainSession == room.mxSession && [self->mxCall.room.roomId isEqualToString:room.roomId])
            {
                // The existing room history has been flushed during server sync.
                // Take into account the updated room state
                [self callRoomStateDidChange:nil];
            }
            
        }];
        
        audioSessionRouteChangeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AVAudioSessionRouteChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
            
            [self updateProximityAndSleep];
            
        }];
        
        // Hide video mute on voice call
        self.videoMuteButton.hidden = !call.isVideoCall;
        
        // Hide camera switch on voice call
        self.cameraSwitchButton.hidden = !call.isVideoCall;
        
        _moreButtonForVideo.hidden = !call.isVideoCall;
        _moreButtonForVoice.hidden = call.isVideoCall;
        
        // Observe call state change
        call.delegate = self;

        // Display room call information
        [self callRoomStateDidChange:^{
            [self call:call stateDidChange:call.state reason:nil];
        }];
        
        if (call.isVideoCall && localPreviewContainerView)
        {
            // Access to the camera is mandatory to display the self view
            // Check the permission right now
            NSString *appDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
            [MXKTools checkAccessForMediaType:AVMediaTypeVideo
                          manualChangeMessage:[VectorL10n cameraAccessNotGrantedForCall:appDisplayName]

                    showPopUpInViewController:self completionHandler:^(BOOL granted) {

                   if (granted)
                   {
                       self->localPreviewContainerView.hidden = NO;
                       self->remotePreviewContainerView.hidden = NO;

                       call.selfVideoView = self->localPreviewVideoView;
                       call.remoteVideoView = self->remotePreviewContainerView;
                       [self applyDeviceOrientation:YES];

                       [[NSNotificationCenter defaultCenter] addObserver:self
                                                                selector:@selector(deviceOrientationDidChange)
                                                                    name:UIDeviceOrientationDidChangeNotification
                                                                  object:nil];
                   }
               }];
        }
        else
        {
            localPreviewContainerView.hidden = YES;
            remotePreviewContainerView.hidden = YES;
        }
    }
}

- (void)setMxCallOnHold:(MXCall *)callOnHold
{
    if (mxCallOnHold == callOnHold)
    {
        //  setting same property, return
        return;
    }
    
    mxCallOnHold = callOnHold;
    
    if (mxCallOnHold)
    {
        self.onHoldCallContainerView.hidden = NO;
        [self.onHoldCallContainerView addGestureRecognizer:self.onHoldCallContainerTapRecognizer];
        [self.onHoldCallContainerView setUserInteractionEnabled:YES];
        
        // Handle peer here
        if (mxCallOnHold.isIncoming)
        {
            self.peerOnHold = [mxCallOnHold.room.mxSession getOrCreateUser:mxCallOnHold.callerId];
        }
        else
        {
            // For 1:1 call, find the other peer
            // Else, the room information will be used to display information about the call
            MXWeakify(self);
            [mxCallOnHold.room state:^(MXRoomState *roomState) {
                MXStrongifyAndReturnIfNil(self);
            
                MXUser *theMember = nil;
                NSArray *members = roomState.members.joinedMembers;
                for (MXUser *member in members)
                {
                    if (![member.userId isEqualToString:self->mxCallOnHold.callerId])
                    {
                        theMember = member;
                        break;
                    }
                }

                self.peerOnHold = theMember;
            }];
        }
    }
    else
    {
        [self.onHoldCallContainerView removeGestureRecognizer:self.onHoldCallContainerTapRecognizer];
        [self.onHoldCallContainerView setUserInteractionEnabled:NO];
        self.onHoldCallContainerView.hidden = YES;
        self.peerOnHold = nil;
    }
}

- (void)setPeer:(MXUser *)peer
{
    _peer = peer;
    
    [self updatePeerInfoDisplay];
}

- (void)setPeerOnHold:(MXUser *)peerOnHold
{
    _peerOnHold = peerOnHold;
    
    NSString *peerAvatarURL;
    
    if (_peerOnHold)
    {
        peerAvatarURL = _peerOnHold.avatarUrl;
    }
    else if (mxCall.isConferenceCall)
    {
        peerAvatarURL = mxCallOnHold.room.summary.avatar;
    }
    
    onHoldCallerImageView.imageView.contentMode = UIViewContentModeScaleAspectFill;
    
    if (peerAvatarURL)
    {
        // Suppose avatar url is a matrix content uri, we use SDK to get the well adapted thumbnail from server
        onHoldCallerImageView.mediaFolder = kMXMediaManagerAvatarThumbnailFolder;
        onHoldCallerImageView.enableInMemoryCache = YES;
        [onHoldCallerImageView setImageURI:peerAvatarURL
                                  withType:nil
                       andImageOrientation:UIImageOrientationUp
                             toFitViewSize:onHoldCallerImageView.frame.size
                                withMethod:MXThumbnailingMethodCrop
                              previewImage:self.picturePlaceholder
                              mediaManager:self.mainSession.mediaManager];
    }
    else
    {
        onHoldCallerImageView.image = self.picturePlaceholder;
    }
}

- (void)updatePeerInfoDisplay
{
    NSString *peerAvatarURL;
    
    if (_peer)
    {
        peerDisplayName = [_peer displayname];
        if (!peerDisplayName.length)
        {
            peerDisplayName = _peer.userId;
        }
        peerAvatarURL = _peer.avatarUrl;
    }
    else if (mxCall.isConferenceCall)
    {
        peerDisplayName = mxCall.room.summary.displayName;
        peerAvatarURL = mxCall.room.summary.avatar;
    }
    
    if (mxCall.isConsulting)
    {
        callerNameLabel.text = [VectorL10n callConsultingWithUser:peerDisplayName];
    }
    else
    {
        if (mxCall.isVideoCall)
        {
            callerNameLabel.text = [VectorL10n callVideoWithUser:peerDisplayName];
        }
        else
        {
            callerNameLabel.text = [VectorL10n callVoiceWithUser:peerDisplayName];
        }
    }
    
    if (peerAvatarURL)
    {
        // Suppose avatar url is a matrix content uri, we use SDK to get the well adapted thumbnail from server
        callerImageView.mediaFolder = kMXMediaManagerAvatarThumbnailFolder;
        callerImageView.enableInMemoryCache = YES;
        [callerImageView setImageURI:peerAvatarURL
                            withType:nil
                 andImageOrientation:UIImageOrientationUp
                       toFitViewSize:callerImageView.frame.size
                          withMethod:MXThumbnailingMethodCrop
                        previewImage:self.picturePlaceholder
                        mediaManager:self.mainSession.mediaManager];
    }
    else
    {
        callerImageView.image = self.picturePlaceholder;
    }
    
    // Round caller image view
    [callerImageView.layer setCornerRadius:callerImageView.frame.size.width / 2];
    callerImageView.clipsToBounds = YES;
}

- (void)setIsRinging:(Boolean)isRinging
{
    if (_isRinging != isRinging)
    {
        if (isRinging)
        {
            NSURL *audioUrl;
            if (mxCall.isIncoming)
            {
                if (self.playRingtone)
                    audioUrl = [self audioURLWithName:@"ring"];
            }
            else
            {
                audioUrl = [self audioURLWithName:@"ringback"];
            }
            
            if (audioUrl)
            {
                [[MXKSoundPlayer sharedInstance] playSoundAt:audioUrl repeat:YES vibrate:mxCall.isIncoming routeToBuiltInReceiver:!mxCall.isIncoming];
            }
        }
        else
        {
            [[MXKSoundPlayer sharedInstance] stopPlayingWithAudioSessionDeactivation:NO];
        }
        
        _isRinging = isRinging;
    }
}

- (void)setDelegate:(id<MXKCallViewControllerDelegate>)delegate
{
    _delegate = delegate;
    
    if (_delegate)
    {
        backToAppButton.hidden = NO;
    }
    else
    {
        backToAppButton.hidden = YES;
    }
}

- (UITapGestureRecognizer *)onHoldCallContainerTapRecognizer
{
    if (_onHoldCallContainerTapRecognizer == nil)
    {
        _onHoldCallContainerTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
                                                                                    action:@selector(onHoldCallContainerTapped:)];
    }
    return _onHoldCallContainerTapRecognizer;
}

- (BOOL)isDisplayingAlert
{
    return errorAlert != nil;
}

- (UIButton *)moreButton
{
    if (mxCall.isVideoCall)
    {
        return _moreButtonForVideo;
    }
    return _moreButtonForVoice;
}

#pragma mark - Sounds

- (NSURL *)audioURLWithName:(NSString *)soundName
{
    return [NSBundle mxk_audioURLFromMXKAssetsBundleWithName:soundName];
}

#pragma mark - Actions

- (void)onHoldCallContainerTapped:(UITapGestureRecognizer *)recognizer
{
    if ([self.delegate respondsToSelector:@selector(callViewControllerDidTapOnHoldCall:)])
    {
        [self.delegate callViewControllerDidTapOnHoldCall:self];
    }
}

- (IBAction)onButtonPressed:(id)sender
{
    if (sender == answerCallButton)
    {
        // If we are here, we have access to the camera
        // The following check is mainly to check microphone access permission
        NSString *appDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];

        [MXKTools checkAccessForCall:mxCall.isVideoCall
         manualChangeMessageForAudio:[VectorL10n microphoneAccessNotGrantedForCall:appDisplayName]
         manualChangeMessageForVideo:[VectorL10n cameraAccessNotGrantedForCall:appDisplayName]
           showPopUpInViewController:self completionHandler:^(BOOL granted) {

               if (granted)
               {
                   [self->mxCall answer];
               }
           }];
    }
    else if (sender == rejectCallButton || sender == endCallButton)
    {
        if (mxCall.state != MXCallStateEnded)
        {
            [mxCall hangup];
        }
        else
        {
            [self dismiss];
        }
    }
    else if (sender == audioMuteButton)
    {
        mxCall.audioMuted = !mxCall.audioMuted;
        audioMuteButton.selected = mxCall.audioMuted;
    }
    else if (sender == videoMuteButton)
    {
        mxCall.videoMuted = !mxCall.videoMuted;
        videoMuteButton.selected = mxCall.videoMuted;
    }
    else if (sender == _resumeButton)
    {
        [mxCall hold:NO];
    }
    else if (sender == self.moreButton)
    {
        [currentAlert dismissViewControllerAnimated:NO completion:nil];
        
        MXWeakify(self);
        
        NSMutableArray<UIAlertAction *> *actions = [NSMutableArray arrayWithCapacity:4];
        
        if (self.speakerButton == nil)
        {
            //  audio device action
            UIAlertAction *audioDeviceAction = [UIAlertAction actionWithTitle:[VectorL10n callMoreActionsChangeAudioDevice]
                                                                        style:UIAlertActionStyleDefault
                                                                      handler:^(UIAlertAction * action) {
                
                MXStrongifyAndReturnIfNil(self);
                self->currentAlert = nil;
                [self showAudioDeviceOptions];
                
            }];
            
            [actions addObject:audioDeviceAction];
        }
        
        //  check the call can be up/downgraded
        
        //  check the call can send DTMF tones
        if (self.mxCall.supportsDTMF)
        {
            UIAlertAction *dialpadAction = [UIAlertAction actionWithTitle:[VectorL10n callMoreActionsDialpad]
                                                                    style:UIAlertActionStyleDefault
                                                                  handler:^(UIAlertAction * action) {
                
                MXStrongifyAndReturnIfNil(self);
                self->currentAlert = nil;
                [self openDialpad];
                
            }];
            
            [actions addObject:dialpadAction];
        }
        
        //  check the call be holded/unholded
        if (mxCall.supportsHolding)
        {
            NSString *actionLocKey = (mxCall.state == MXCallStateOnHold) ? [VectorL10n callMoreActionsUnhold] : [VectorL10n callMoreActionsHold];
            
            UIAlertAction *holdAction = [UIAlertAction actionWithTitle:actionLocKey
                                                                 style:UIAlertActionStyleDefault
                                                               handler:^(UIAlertAction * action) {
                
                MXStrongifyAndReturnIfNil(self);
                self->currentAlert = nil;
                [self->mxCall hold:(self.mxCall.state != MXCallStateOnHold)];
                
            }];
            
            [actions addObject:holdAction];
        }
        
        //  check the call be transferred
        if (mxCall.supportsTransferring && self.peer)
        {
            UIAlertAction *transferAction = [UIAlertAction actionWithTitle:[VectorL10n callMoreActionsTransfer]
                                                                     style:UIAlertActionStyleDefault
                                                                   handler:^(UIAlertAction * action) {
                
                MXStrongifyAndReturnIfNil(self);
                self->currentAlert = nil;
                
                [self openCallTransfer];
            }];
            
            [actions addObject:transferAction];
        }
        
        if (actions.count > 0)
        {
            //  create the alert
            currentAlert = [UIAlertController alertControllerWithTitle:nil
                                                               message:nil
                                                        preferredStyle:UIAlertControllerStyleActionSheet];
            
            //  add actions
            [actions enumerateObjectsUsingBlock:^(UIAlertAction * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
                [currentAlert addAction:obj];
            }];
            
            //  add cancel action always
            [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
                                                             style:UIAlertActionStyleCancel
                                                           handler:^(UIAlertAction * action) {
                
                MXStrongifyAndReturnIfNil(self);
                self->currentAlert = nil;
                
            }]];
            
            [currentAlert popoverPresentationController].sourceView = self.moreButton;
            [currentAlert popoverPresentationController].sourceRect = self.moreButton.bounds;
            [self presentViewController:currentAlert animated:YES completion:nil];
        }
    }
    else if (sender == speakerButton)
    {
        [self showAudioDeviceOptions];
    }
    else if (sender == cameraSwitchButton)
    {
        switch (mxCall.cameraPosition)
        {
            case AVCaptureDevicePositionFront:
                mxCall.cameraPosition = AVCaptureDevicePositionBack;
                break;
                
            default:
                mxCall.cameraPosition = AVCaptureDevicePositionFront;
                break;
        }
    }
    else if (sender == backToAppButton)
    {
        if (_delegate)
        {
            // Dismiss the view controller whereas the call is still running
            [_delegate dismissCallViewController:self completion:nil];
        }
    }
    else if (sender == _transferButton)
    {
        //  actually transfer the call without consulting
        [self.mainSession.callManager transferCall:mxCall.callWithTransferee
                                                to:mxCall.transferTarget
                                    withTransferee:mxCall.transferee
                                      consultFirst:NO
                                           success:^(NSString * _Nullable newCallId) {
            
        }
                                           failure:^(NSError * _Nullable error) {
            
        }];
    }
    
    [self updateProximityAndSleep];
}

- (void)showAudioDeviceOptions
{
    NSMutableArray<UIAlertAction *> *actions = [NSMutableArray new];
    NSArray<MXiOSAudioOutputRoute *> *availableRoutes = mxCall.audioOutputRouter.availableOutputRoutes;
    
    for (MXiOSAudioOutputRoute *route in availableRoutes)
    {
        //  route action
        NSString *name = route.name;
        if (route.routeType == MXiOSAudioOutputRouteTypeLoudSpeakers)
        {
            name = [VectorL10n callMoreActionsAudioUseDevice];
        }
        MXWeakify(self);
        UIAlertAction *routeAction = [UIAlertAction actionWithTitle:name
                                                              style:UIAlertActionStyleDefault
                                                            handler:^(UIAlertAction * action) {
            
            MXStrongifyAndReturnIfNil(self);
            self->currentAlert = nil;
            [self->mxCall.audioOutputRouter changeCurrentRouteTo:route];
            
        }];
        
        [actions addObject:routeAction];
    }
    
    if (actions.count > 0)
    {
        //  create the alert
        currentAlert = [UIAlertController alertControllerWithTitle:nil
                                                           message:nil
                                                    preferredStyle:UIAlertControllerStyleActionSheet];
        
        for (UIAlertAction *action in actions)
        {
            [currentAlert addAction:action];
        }
        
        //  add cancel action
        MXWeakify(self);
        [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
                                                         style:UIAlertActionStyleCancel
                                                       handler:^(UIAlertAction * action) {
            
            MXStrongifyAndReturnIfNil(self);
            self->currentAlert = nil;
            
        }]];
        
        [currentAlert popoverPresentationController].sourceView = self.moreButton;
        [currentAlert popoverPresentationController].sourceRect = self.moreButton.bounds;
        [self presentViewController:currentAlert animated:YES completion:nil];
    }
}
    
#pragma mark - DTMF

- (void)openDialpad
{
    //  no-op
}

#pragma mark - Call Transfer

- (void)openCallTransfer
{
    //  no-op
}

#pragma mark - MXCallDelegate

- (void)call:(MXCall *)call stateDidChange:(MXCallState)state reason:(MXEvent *)event
{
    // Set default configuration of bottom bar
    endCallButton.hidden = NO;
    rejectCallButton.hidden = YES;
    answerCallButton.hidden = YES;
    self.moreButton.enabled = YES;
    _resumeButton.hidden = state != MXCallStateOnHold;
    _pausedIcon.hidden = state != MXCallStateOnHold && state != MXCallStateRemotelyOnHold;
    _transferButton.hidden = YES;
    
    [localPreviewActivityView stopAnimating];
    
    switch (state)
    {
        case MXCallStateFledgling:
            self.isRinging = NO;
            callStatusLabel.text = [VectorL10n callConnecting];
            break;
        case MXCallStateWaitLocalMedia:
            self.isRinging = NO;
            [self configureSpeakerButton];
            [localPreviewActivityView startAnimating];
            
            // Try to show a special view for incoming view
            [self configureIncomingCallViewIfRequiredWith:call];
            
            break;
        case MXCallStateCreateOffer:
        {
            // When CallKit is enabled and we have an outgoing call, we need to start playing ringback sound
            // only after AVAudioSession will be activated by the system otherwise the sound will be gone.
            // We always receive signal about MXCallStateCreateOffer earlier than the system activates AVAudioSession
            // so we start playing ringback sound only on AVAudioSession activation in handleAudioSessionActivationNotification
            BOOL isCallKitAvailable = [MXCallKitAdapter callKitAvailable] && [MXKAppSettings standardAppSettings].isCallKitEnabled;
            if (!isCallKitAvailable)
            {
                self.isRinging = YES;
            }
            
            callStatusLabel.text = [VectorL10n callConnecting];
            break;
        }
        case MXCallStateInviteSent:
        {
            callStatusLabel.text = [VectorL10n callRinging];
            break;
        }
        case MXCallStateRinging:
            self.isRinging = YES;
            [self configureSpeakerButton];
            if (call.isVideoCall)
            {
                callStatusLabel.text = [VectorL10n incomingVideoCall];
            }
            else
            {
                callStatusLabel.text = [VectorL10n incomingVoiceCall];
            }
            // Update bottom bar
            endCallButton.hidden = YES;
            rejectCallButton.hidden = NO;
            answerCallButton.hidden = NO;
            
            // Try to show a special view for incoming view
            [self configureIncomingCallViewIfRequiredWith:call];
            
            break;
        case MXCallStateConnecting:
            self.isRinging = NO;
            
            // User has accepted the call and we can remove incomingCallView
            if (self.incomingCallView)
            {
                [UIView transitionWithView:self.view
                                  duration:0.33
                                   options:UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionCurveEaseOut
                                animations:^{
                                    [self.incomingCallView removeFromSuperview];
                                }
                                completion:^(BOOL finished) {
                                    self.incomingCallView = nil;
                                }];
            }
            
            break;
        case MXCallStateConnected:
            self.isRinging = NO;
            [self updateTimeStatusLabel];

            if (call.isVideoCall)
            {
                self.callerImageView.hidden = YES;
                
                if (call.isConferenceCall)
                {
                    // Do not show self view anymore because it is returned by the conference bridge
                    self.localPreviewContainerView.hidden = YES;

                    // Well, hide does not work. So, shrink the view to nil
                    self.localPreviewContainerView.frame = CGRectZero;
                }
            }
            audioMuteButton.enabled = YES;
            videoMuteButton.enabled = YES;
            speakerButton.enabled = YES;
            cameraSwitchButton.enabled = YES;
            if (call.isConsulting)
            {
                _transferButton.hidden = NO;
            }

            break;
        case MXCallStateOnHold:
            callStatusLabel.text = [VectorL10n callHolded];
            
            break;
        case MXCallStateRemotelyOnHold:
            audioMuteButton.enabled = NO;
            videoMuteButton.enabled = NO;
            speakerButton.enabled = NO;
            cameraSwitchButton.enabled = NO;
            self.moreButton.enabled = NO;
            callStatusLabel.text = [VectorL10n callRemoteHolded:peerDisplayName];
            
            break;
        case MXCallStateInviteExpired:
            // MXCallStateInviteExpired state is sent as an notification
            // MXCall will move quickly to the MXCallStateEnded state
            self.isRinging = NO;
            callStatusLabel.text = [VectorL10n callInviteExpired];
            
            break;
        case MXCallStateEnded:
        {
            self.isRinging = NO;
            callStatusLabel.text = [VectorL10n callEnded];
            
            NSString *soundName = [self soundNameForCallEnding];
            if (soundName)
            {
                NSURL *audioUrl = [self audioURLWithName:soundName];
                [[MXKSoundPlayer sharedInstance] playSoundAt:audioUrl repeat:NO vibrate:NO routeToBuiltInReceiver:YES];
            }
            else
            {
                [[MXKSoundPlayer sharedInstance] stopPlayingWithAudioSessionDeactivation:YES];
            }
            
            // Except in case of call error, quit the screen right now
            if (!errorAlert)
            {
                [self dismiss];
            }

            break;
        }
        default:
            break;
    }
    
    [self updateProximityAndSleep];
}

- (void)call:(MXCall *)call didEncounterError:(NSError *)error reason:(MXCallHangupReason)reason
{
    MXLogDebug(@"[MXKCallViewController] didEncounterError. mxCall.state: %tu. Stop call due to error: %@", mxCall.state, error);

    if (mxCall.state != MXCallStateEnded)
    {
        // Popup the error to the user
        NSString *title = [error.userInfo valueForKey:NSLocalizedFailureReasonErrorKey];
        if (!title)
        {
            title = [VectorL10n error];
        }
        NSString *msg = [error.userInfo valueForKey:NSLocalizedDescriptionKey];
        if (!msg)
        {
            msg = [VectorL10n errorCommonMessage];
        }

        MXWeakify(self);
        errorAlert = [UIAlertController alertControllerWithTitle:title message:msg preferredStyle:UIAlertControllerStyleAlert];
        
        [errorAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
                                                       style:UIAlertActionStyleDefault
                                                     handler:^(UIAlertAction * action) {
            
            MXStrongifyAndReturnIfNil(self);
            self->errorAlert = nil;
            [self dismiss];
            
        }]];
        
        [self presentViewController:errorAlert animated:YES completion:nil];
        
        // And interrupt the call
        [mxCall hangupWithReason:reason];
    }
}

- (void)callConsultingStatusDidChange:(MXCall *)call
{
    [self updatePeerInfoDisplay];
    
    if (call.isConsulting)
    {
        NSString *title = [VectorL10n callTransferToUser:call.transferee.displayname];
        [_transferButton setTitle:title forState:UIControlStateNormal];
        _transferButton.hidden = call.state != MXCallStateConnected;
    }
    else
    {
        _transferButton.hidden = YES;
    }
}

- (void)callAssertedIdentityDidChange:(MXCall *)call
{
    MXAssertedIdentityModel *assertedIdentity = call.assertedIdentity;
    
    if (assertedIdentity)
    {
        //  update caller display name and avatar with the asserted identity
        NSString *peerAvatarURL = assertedIdentity.avatarUrl;
        
        if (assertedIdentity.displayname)
        {
            peerDisplayName = assertedIdentity.displayname;
        }
        else if (assertedIdentity.userId)
        {
            peerDisplayName = assertedIdentity.userId;
        }
        
        if (mxCall.isVideoCall)
        {
            callerNameLabel.text = [VectorL10n callVideoWithUser:peerDisplayName];
        }
        else
        {
            callerNameLabel.text = [VectorL10n callVoiceWithUser:peerDisplayName];
        }
        
        if (peerAvatarURL)
        {
            // Suppose avatar url is a matrix content uri, we use SDK to get the well adapted thumbnail from server
            callerImageView.mediaFolder = kMXMediaManagerAvatarThumbnailFolder;
            callerImageView.enableInMemoryCache = YES;
            [callerImageView setImageURI:peerAvatarURL
                                withType:nil
                     andImageOrientation:UIImageOrientationUp
                           toFitViewSize:callerImageView.frame.size
                              withMethod:MXThumbnailingMethodCrop
                            previewImage:self.picturePlaceholder
                            mediaManager:self.mainSession.mediaManager];
        }
        else
        {
            callerImageView.image = self.picturePlaceholder;
        }
        
        [updateStatusTimer fire];
    }
    else
    {
        //  go back to the original display name and avatar
        [self updatePeerInfoDisplay];
    }
}

- (void)callAudioOutputRouteTypeDidChange:(MXCall *)call
{
    [self configureSpeakerButton];
}

- (void)callAvailableAudioOutputsDidChange:(MXCall *)call
{
    
}

#pragma mark - Internal

- (void)removeObservers
{
    if (roomDidFlushDataNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:roomDidFlushDataNotificationObserver];
        roomDidFlushDataNotificationObserver = nil;
    }
    
    if (audioSessionRouteChangeNotificationObserver)
    {
        [[NSNotificationCenter defaultCenter] removeObserver:audioSessionRouteChangeNotificationObserver];
        audioSessionRouteChangeNotificationObserver = nil;
    }
    
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    
    if (roomListener && mxCall.room)
    {
        MXWeakify(self);
        [mxCall.room liveTimeline:^(id<MXEventTimeline> liveTimeline) {
            MXStrongifyAndReturnIfNil(self);

            [liveTimeline removeListener:self->roomListener];
            self->roomListener = nil;
        }];
    }
}

- (void)callRoomStateDidChange:(dispatch_block_t)onComplete
{
    // Handle peer here
    if (mxCall.isIncoming)
    {
        self.peer = [mxCall.room.mxSession getOrCreateUser:mxCall.callerId];
        if (onComplete)
        {
            onComplete();
        }
    }
    else
    {
        // For 1:1 call, find the other peer
        // Else, the room information will be used to display information about the call
        if (!mxCall.isConferenceCall)
        {
            MXWeakify(self);
            [mxCall.room state:^(MXRoomState *roomState) {
                MXStrongifyAndReturnIfNil(self);
            
                MXUser *theMember = nil;
                NSArray *members = roomState.members.joinedMembers;
                for (MXUser *member in members)
                {
                    if (![member.userId isEqualToString:self->mxCall.callerId])
                    {
                        theMember = member;
                        break;
                    }
                }

                self.peer = theMember;
                if (onComplete)
                {
                    onComplete();
                }
            }];
        }
        else
        {
            self.peer = nil;
            if (onComplete)
            {
                onComplete();
            }
        }
    }
}

- (BOOL)isBuiltInReceiverAudioOuput
{
#if TARGET_IPHONE_SIMULATOR
    return YES;
#endif
    BOOL isBuiltInReceiverUsed = NO;
    
    // Check whether the audio output is the built-in receiver
    AVAudioSessionRouteDescription *audioRoute = [[AVAudioSession sharedInstance] currentRoute];
    if (audioRoute.outputs.count)
    {
        // TODO: handle the case where multiple outputs are returned
        AVAudioSessionPortDescription *audioOutputs = audioRoute.outputs.firstObject;
        isBuiltInReceiverUsed = ([audioOutputs.portType isEqualToString:AVAudioSessionPortBuiltInReceiver]);
    }
    
    return isBuiltInReceiverUsed;
}

- (NSString *)soundNameForCallEnding
{
    if (mxCall.endReason == MXCallEndReasonUnknown)
        return nil;
    
    if (mxCall.isEstablished)
        return @"callend";
    
    if (mxCall.endReason == MXCallEndReasonBusy || (!mxCall.isIncoming && mxCall.endReason == MXCallEndReasonMissed))
        return @"busy";
    
    return nil;
}

- (void)handleAudioSessionActivationNotification
{
    // It's only relevant for outgoing calls which aren't in connected state
    if (self.mxCall.state >= MXCallStateCreateOffer && self.mxCall.state != MXCallStateConnected && self.mxCall.state != MXCallStateEnded)
    {
        self.isRinging = YES;
    }
}

#pragma mark - UI methods

- (void)configureSpeakerButton
{
    switch (mxCall.audioOutputRouter.currentRoute.routeType)
    {
        case MXiOSAudioOutputRouteTypeBuiltIn:
            self.speakerButton.selected = NO;
            break;
        case MXiOSAudioOutputRouteTypeLoudSpeakers:
        case MXiOSAudioOutputRouteTypeExternalWired:
        case MXiOSAudioOutputRouteTypeExternalBluetooth:
        case MXiOSAudioOutputRouteTypeExternalCar:
            self.speakerButton.selected = YES;
            break;
    }
}

- (void)configureIncomingCallViewIfRequiredWith:(MXCall *)call
{
    if (call.isIncoming && !self.incomingCallView)
    {
        UIView *incomingCallView = [self createIncomingCallView];
        if (incomingCallView)
        {
            self.incomingCallView = incomingCallView;
            [self.view addSubview:incomingCallView];
            
            incomingCallView.translatesAutoresizingMaskIntoConstraints = NO;
            [incomingCallView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:0].active = YES;
            [incomingCallView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:0].active = YES;
            [incomingCallView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:0].active = YES;
            [incomingCallView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:0].active = YES;
        }
    }
}

- (void)updateLocalPreviewLayout
{
    // On IOS 8 and later, the screen size is oriented.
    CGRect bounds = [[UIScreen mainScreen] bounds];
    BOOL isLandscapeOriented = (bounds.size.width > bounds.size.height);
    
    CGFloat maxPreviewFrameSize, minPreviewFrameSize;
    
    if (_localPreviewContainerViewWidthConstraint.constant < _localPreviewContainerViewHeightConstraint.constant)
    {
        maxPreviewFrameSize = _localPreviewContainerViewHeightConstraint.constant;
        minPreviewFrameSize = _localPreviewContainerViewWidthConstraint.constant;
    }
    else
    {
        minPreviewFrameSize = _localPreviewContainerViewHeightConstraint.constant;
        maxPreviewFrameSize = _localPreviewContainerViewWidthConstraint.constant;
    }
    
    if (isLandscapeOriented)
    {
        _localPreviewContainerViewHeightConstraint.constant = minPreviewFrameSize;
        _localPreviewContainerViewWidthConstraint.constant = maxPreviewFrameSize;
    }
    else
    {
        _localPreviewContainerViewHeightConstraint.constant = maxPreviewFrameSize;
        _localPreviewContainerViewWidthConstraint.constant = minPreviewFrameSize;
    }
    
    CGPoint previewOrigin = self.localPreviewContainerView.frame.origin;
    
    if (previewOrigin.x != (bounds.size.width - _localPreviewContainerViewWidthConstraint.constant - kLocalPreviewMargin))
    {
        CGFloat posX = (bounds.size.width - _localPreviewContainerViewWidthConstraint.constant - kLocalPreviewMargin);
        _localPreviewContainerViewLeadingConstraint.constant = posX;
    }
    
    if (previewOrigin.y != kLocalPreviewMargin)
    {
        CGFloat posY = (bounds.size.height - _localPreviewContainerViewHeightConstraint.constant - kLocalPreviewMargin);
        _localPreviewContainerViewTopConstraint.constant = posY;
    }
}

- (void)showOverlayContainer:(BOOL)isShown
{
    if (mxCall && !mxCall.isVideoCall) isShown = YES;
    if (mxCall.state != MXCallStateConnected) isShown = YES;
    
    if (isShown)
    {
        overlayContainerView.hidden = NO;
        if (mxCall && mxCall.isVideoCall)
        {
            [hideOverlayTimer invalidate];
            hideOverlayTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(hideOverlay:) userInfo:nil repeats:NO];
        }
    }
    else
    {
        overlayContainerView.hidden = YES;
    }
}

- (void)toggleOverlay
{
    [self showOverlayContainer:overlayContainerView.isHidden];
}

- (void)hideOverlay:(NSTimer*)theTimer
{
    [self showOverlayContainer:NO];
    hideOverlayTimer = nil;
}

- (void)updateTimeStatusLabel
{
    if (mxCall.state == MXCallStateConnected)
    {
        NSUInteger duration = mxCall.duration / 1000;
        NSUInteger secs = duration % 60;
        NSUInteger mins = (duration - secs) / 60;
        callStatusLabel.text = [NSString stringWithFormat:@"%02tu:%02tu", mins, secs];
    }
}

 - (void)updateProximityAndSleep
 {
     BOOL inCall = (mxCall.state == MXCallStateConnected || mxCall.state == MXCallStateRinging || mxCall.state == MXCallStateInviteSent || mxCall.state == MXCallStateConnecting || mxCall.state == MXCallStateCreateOffer || mxCall.state == MXCallStateCreateAnswer);

     BOOL isBuiltInReceiverUsed = self.isBuiltInReceiverAudioOuput;
     
     // Enable the proximity monitoring when the built in receiver is used as the audio output.
     BOOL enableProxMonitoring = inCall && isBuiltInReceiverUsed;
     
     UIDevice *device = [UIDevice currentDevice];
     if (device && device.isProximityMonitoringEnabled != enableProxMonitoring)
     {
         [device setProximityMonitoringEnabled:enableProxMonitoring];
     }

     // Disable the idle timer during a video call, or during a voice call which is performed with the built-in receiver.
     // Note: if the device is locked, VoIP calling get dropped if an incoming GSM call is received.
     BOOL disableIdleTimer = inCall && (mxCall.isVideoCall || isBuiltInReceiverUsed);
     
     UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)];
     if (sharedApplication && sharedApplication.isIdleTimerDisabled != disableIdleTimer)
     {
         sharedApplication.idleTimerDisabled = disableIdleTimer;
     }
 }

- (UIView *)createIncomingCallView
{
    return nil;
}

#pragma mark - UIResponder Touch Events

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint point = [touch locationInView:self.view];
    if ((!self.localPreviewContainerView.hidden) && CGRectContainsPoint(self.localPreviewContainerView.frame, point))
    {
        // Starting to move the local preview view
        if (mxCallOnHold)
        {
            //  if there is a call on hold, do not move local preview for now
            //  TODO: Instead of wholly avoiding mobility of local preview, just avoid the on hold call's corner here
            return;
        }
        isSelectingLocalPreview = YES;
    }
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    isMovingLocalPreview = NO;
    isSelectingLocalPreview = NO;
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    if (isMovingLocalPreview)
    {
        UITouch *touch = [touches anyObject];
        CGPoint point = [touch locationInView:self.view];
        
        CGRect bounds = self.view.bounds;
        CGFloat midX = bounds.size.width / 2.0;
        CGFloat midY = bounds.size.height / 2.0;
        
        CGFloat posX = (point.x < midX) ? 20.0 : (bounds.size.width - _localPreviewContainerViewWidthConstraint.constant - 20.0);
        CGFloat posY = (point.y < midY) ? 20.0 : (bounds.size.height - _localPreviewContainerViewHeightConstraint.constant - 20.0);
        
        _localPreviewContainerViewLeadingConstraint.constant = posX;
        _localPreviewContainerViewTopConstraint.constant = posY;
        
        [self.view setNeedsUpdateConstraints];
    }
    else
    {
        [self toggleOverlay];
    }
    isMovingLocalPreview = NO;
    isSelectingLocalPreview = NO;
}

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch = [touches anyObject];
    CGPoint point = [touch locationInView:self.view];
    
    if (isSelectingLocalPreview)
    {
        isMovingLocalPreview = YES;
        self.localPreviewContainerView.center = point;
    }
}

#pragma mark - UIDeviceOrientationDidChangeNotification

- (void)deviceOrientationDidChange
{
    [self applyDeviceOrientation:NO];
    
    [self showOverlayContainer:YES];
}

- (void)applyDeviceOrientation:(BOOL)forcePortrait
{
    if (mxCall)
    {
        UIDeviceOrientation deviceOrientation = [[UIDevice currentDevice] orientation];
        
        // Set the camera orientation according to the orientation supported by the app
        if (UIDeviceOrientationPortrait == deviceOrientation || UIDeviceOrientationLandscapeLeft == deviceOrientation || UIDeviceOrientationLandscapeRight == deviceOrientation)
        {
            mxCall.selfOrientation = deviceOrientation;
            [self updateLocalPreviewLayout];
        }
        else if (forcePortrait)
        {
            mxCall.selfOrientation = UIDeviceOrientationPortrait;
            [self updateLocalPreviewLayout];
        }        
    }
}

@end
